Graphics and Shaders

Channels

  • Martin Donald .

    • Shaders and techniques in Godot.

  • Crigz vs Game Dev .

    • Shaders and techniques in Godot.

    • Also has tutorials on Physics and Procedural Animations.

  • Acerola .

    • Shaders.

  • Pixezy .

  • SimonDev .

    • Rendering techniques, graphics optimization, and random stuff.

  • GameArtTricks .

    • Various tricks used in games.

  • I saw somethings about godot shaders in (2023-07-10), and went back to study this in (2025-09-18), when learning Vulkan.

Math, Linear Algebra

  • Essence of Linear Algebra .

    • Very good.

    • Watched everything, but skipped the eigenvector and eigenvalues part, as it wasn't relevant for what I'm doing.

Matrix

  • .

    • Maps 2 dimensions to 3 dimensions.

  • .

    • Maps 3 dimensions to 2 dimensions.

  • Matrix multiplication :

    • Matrices give us a language to describe transformations, where the columns represent those coordinates.

    • Matrix multiplication is just a way to compute what this transformation does to a given vector.

    • Any matrix can be interpreted as a transformation of space.

    • .

  • Matrix composition :

    • .

      • i (green)  will point to 1, 1 .

      • j (red)  will point to -1, 0 .

      • This constitutes a Rotation + Shear.

    • .

      • First apply the rotation, and then apply the shear.

      • This is the same as the composition 'rotation + shear'.

    • .

      • A composition has the same meaning as applying one transformation, then another.

      • This is read from right to left: the first transformation is Rotation, then Shear.

      • This comes from function notation f(g(x)) .

    • .

      • This is true, as you are just applying C, then B, then A, either way.

  • Determinant :

    • How much the AREA has scaled.

      • Or VOLUME if in 3D; imagine a 1x1x1 cube.

    • .

    • .

      • If the determinant is zero, it means that it squashes everything into a lower dimension.

    • If the determinant is negative, it means that the orientation is flipped (the space turns on itself) (a sheet of paper flips).

    • Calculation :

      • 2D:

        • .

        • Proof: .

      • 3D:

        • .

        • Proof: 'It's complicated and doesn't really make sense as the "essence of linear algebra"'.

  • System of equations :

    • .

    • "Rank": number of dimensions in the output of a transformation.

    • "Column Space": Set of all possible outputs Av

  • Inverse Matrix :

    • Does the inverse of a transformation.

      • If a matrix rotates 90Âș clockwise, the inverse of this would be rotating 90Âș counter-clockwise.

    • .

      • Doing the transformation A, and then doing the inverse of A, it's the same as not doing anything; hence the Identity Matrix.

    • OpenGL Normal Mapping Quote:

      • Note that we use the transpose function instead of the inverse function here. A great property of orthogonal matrices (each axis is a perpendicular unit vector) is that the transpose of an orthogonal matrix equals its inverse. This is a great property as inverse is expensive and a transpose isn't.

  • Dot Product :

    • .

    • Usually used to analyze the angle between two vectors:

      • a.b > 0 : angle less than 90Âș.

      • a.b = 0 : angle equal to 90Âș.

      • a.b < 0 : angle greater than 90Âș.

      • If the value is positive, it means the vectors point "in the same direction".

  • Cross Product :

    • .

    • Calculating:

      • The Cross Product is just the determinant of the matrix, as the determinant computes the AREA of the matrix.

        • Also, consider the orientation.

      • .

        • This is a mathematical trick to "help" calculate the determinant above.

    • Orientation:

      • .

      • .

      • .

  • Eigenvector and Eigenvalues :

    • Eigenvector: A vector that remains in its own span.

    • .

Quaternions

  • Zero rotation :

    • (0, 0, 0, 1) .

  • Quaternion multiplication :

    • Is rotation composition.

    • In the context of 3D rotations, multiplication is the only operation that directly corresponds to combining or applying rotations.

    • Non-commutativity matters: q2q1≠q1q2 . This matches the non-commutativity of rotation matrices.

  • Quaternion conjugate / inverse :

    • The conjugate of a unit quaternion is its inverse.

    • Needed when rotating vectors: $qvq^{−1}$.

    • Useful for undoing a rotation.

  • Quaternion scalar multiplication :

    • Not used for representing rotations, since only unit quaternions encode valid rotations.

  • Quaternion addition :

    • Is averaging rotation.

    • ChatGPT:

      • Addition of quaternions has no direct geometric meaning for rotations.

      • It can be used in interpolation schemes (e.g., normalized linear interpolation), but addition itself is not a “rotation operation”.

  • Why Use Quaternions in Graphics :

    • Numerical stability : avoids gimbal lock (unlike Euler angles).

    • Compactness : 4 numbers vs 9 in a matrix.

    • Efficient interpolation : spherical linear interpolation (slerp) is defined directly on quaternions, which is critical for animation blending.

    • Fast composition : quaternion multiplication is cheaper than matrix multiplication.

  • Euler to Quaternion :

    • quaternion_from_euler_angles() .

    • Let:

      • roll = α (rotation around X)

      • pitch = ÎČ (rotation around Y)

      • yaw = Îł (rotation around Z)

    • Assume intrinsic rotation order Z → Y → X  (common convention in graphics).

    • Quaternion (w, x, y, z):

      w = cos(α/2) * cos(ÎČ/2) * cos(Îł/2) + sin(α/2) * sin(ÎČ/2) * sin(Îł/2)
      x = sin(α/2) * cos(ÎČ/2) * cos(Îł/2) - cos(α/2) * sin(ÎČ/2) * sin(Îł/2)
      y = cos(α/2) * sin(ÎČ/2) * cos(Îł/2) + sin(α/2) * cos(ÎČ/2) * sin(Îł/2)
      z = cos(α/2) * cos(ÎČ/2) * sin(Îł/2) - sin(α/2) * sin(ÎČ/2) * cos(Îł/2)
      
    • The above assumes Z-Y-X order  (yaw → pitch → roll). If your engine uses a different Euler order, the formulas change.

    • Always normalize the quaternion after conversion from Euler to keep it valid.

  • Quaternion to Euler :

    pitch (ÎČ) = asin( 2 * (w*y - z*x) )
    yaw   (Îł) = atan2( 2 * (w*x + y*z), 1 - 2*(x*x + y*y) )
    roll  (α) = atan2( 2 * (w*z + x*y), 1 - 2*(y*y + z*z) )
    
    • The above assumes Z-Y-X order  (yaw → pitch → roll). If your engine uses a different Euler order, the formulas change.

    • atan2  is the 2-argument arctangent, which handles quadrant correctly.

    • Gimbal lock happens if pitch = ±90°. In that case, roll and yaw are not uniquely defined.

  • ~ Understanding Quaternions for Game Dev .

    • Critiques the explanations by 3blue1brown and numberfy for complicating the concept by introducing 4D spheres.

    • His explanation is based on defining a rotation axis, etc.

    • His intent to simplify the process is fine, but there is no foundational reasoning; it feels like rote memorization.

    • For graphics programming, this is probably the only explanation that makes sense to look at; 3blue1brown's is somewhat irrelevant, curiously.

  • ~ Quaternion and 3D Rotation .

    • Yeah, nothing useful for computer graphics could be understood.

  • ~ Visualization of Quaternions as a 4D sphere .

    • Only provides several images to help visualize the 4D sphere, nothing more.

    • Yeah, nothing useful for computer graphics could be understood.

  • .

Etc

Spherical Coordinates

Polar Coordinates

Wave Function Collapse (WFC)

Spaces, Transformations and Graphics Pipeline

Concepts

  • Homogeneous coordinates (4D) :

  • Affine Transformations :

    • Preserves:

    • Straight lines (no curves introduced)

    • Parallelism (parallel lines remain parallel, though distances can change)

    • Ratios along a line (midpoints stay midpoints)

    • An affine transformation has the last row fixed to [0,0,0,1] .

    • Affine examples :

      • Translation

      • Rotation

      • Uniform/non-uniform scaling

      • Shearing

      • Any combination of the above

    • Non-affine examples :

      • Perspective projection (parallel lines may converge)

      • Non-linear warps

  • Note :

    • 'Matrix' and 'Transform Matrix' are the same thing in this context.

    • Everything is separated between Spaces, Matrices, or operations.

  • .

  • .

Model / Object / Local Space

  • A coordinate space where a single mesh or object’s vertices are defined relative to the object’s own origin.

  • Vertices are usually created and manipulated in object space before any scene-level transforms are applied.

Model / Object / Local Matrix

  • Transforms Model / Object / Local Space  into World Space .

  • It's an affine transformation.

  • Applies the object’s translation, rotation, scale (and optionally shear).

World Space

  • A scene-level coordinate system in which multiple objects are placed and arranged.

  • World space is the reference for lighting, physics, and global placement.

Camera / View / Eye Matrix

  • Transforms World Space  into Camera / View / Eye Space .

  • It's an affine transformation.

  • The inverse of a camera’s world transform.

Camera / View / Eye Space

  • A coordinate system where the camera is at the origin and looks down a canonical axis (commonly −Z or +Z depending on convention).

  • Vertices are expressed relative to the camera: positions are what the camera “sees.”

  • .

Projection Matrix / Classical-Z / Reversed-Z

  • Transforms all vertices from the Camera / View / Eye Space  into Clip Space .

    • The GPU later Clips  to the frustum.

    • The projection matrix doesn’t “ignore” vertices outside the frustum; it still transforms them.

  • Is defined by the View Frustum  parameters.

  • It's not an affine transformation, as they rely on a divide by w  (the Perspective Divide ).

  • Reverse Z (and why it's so awesome) - Tom Hulton-Harrop .

    • The best article on the topic.

  • Vulkan Projection Matrix - Vincent Parizet .

    • The explanation seems correct, but I'm quite sure his formulas use row_major, instead of column major.

  • Ultraviolet Engine - Rust .

    • I'm not sure I should trust this source.

    • perspective_infinite_z_vk  was incorrect.

  • Definitions :

    • vertical_fov

      • Should be provided in radians.

    • aspect_ratio

      • Should be the quotient width / height .

What matters
  • Handedness :

    • It matters.

  • Which axis is vertical in NDC :

    • It matters.

  • Depth range :

    • It matters.

  • Y Up vs Z Up :

    • It doesn't matter.

Classical-Z / Reversed-Z

Vulkan
  • Flipping the Y axis is required because Vulkan's Y axis in clip space points down.

    • Other options :

      • Flipping y in the shader.

      • Setting a negative height for the viewport.

      • With reverse z it’s possible to just flip the order of the near and far arguments when using the regular projection matrix.

      • For whatever reason I prefer the above approach but please do what’s best for you and be aware of these other approaches out in the wild.

  • Classical-Z :

    • Finite :

      • Right Handed :

        • Direct result:

        @(require_results)
        camera_perspective_vulkan_classical_z :: proc "contextless" (vertical_fov, aspect, near, far: f32) -> (m: Mat4) #no_bounds_check {
            /*
            Right-handed (Camera forward -Z).
            Clip Space: left-handed, y-down, with Z (depth) extending from 0.0 (close) to 1.0 (far).
            */
            focal_length := 1.0 / math.tan(0.5 * vertical_fov)
            nmf := near - far
            m[0, 0] = focal_length / aspect
            m[1, 1] = -focal_length
            m[2, 2] = far / nmf
            m[3, 2] = -1
            m[2, 3] = near * far / nmf
            return
        }
        
        • Manually: Take the OpenGL projection matrix, remap the depth from 0 to 1, and then additionally flip the Y axis.

        mat4 perspective_vulkan_rh(const float fovy, const float aspect, const float n, const float f) {
          constexpr mat4 vulkan_clip {1.0f,  0.0f, 0.0f, 0.0f,
                                      0.0f, -1.0f, 0.0f, 0.0f,
                                      0.0f,  0.0f, 0.5f, 0.0f,
                                      0.0f,  0.0f, 0.5f, 1.0f};
          return mat_mul(perspective_opengl_rh(fovy, aspect, n, f), vulkan_clip);
              // The correct multiplication is vulkan_clip * perspective_opengl_rh (column-vector math)
        }
        
    • Infinite :

      • Right Handed :

        @(require_results)
        camera_perspective_vulkan_infinite :: proc "contextless" (vertical_fov, aspect, near: f32) -> (m: Mat4) #no_bounds_check {
            /*
            Right-handed (Camera forward -Z).
            Clip Space: left-handed, y-down, with Z (depth) extending from 1.0 (close) to 0.0 (far).
            */
            focal_length := 1.0 / math.tan(0.5 * vertical_fov)
            A :: -1.0
            B := near
            m[0, 0] = focal_length / aspect
            m[1, 1] = -focal_length
            m[2, 2] = A
            m[3, 2] = -1
            m[2, 3] = B
            return
        }
        
  • Reversed-Z :

    • Clear depth to 0 (not 1 as usual).

    • Set depth test to greater  (not less  as usual).

    • Ensure you’re using a floating point depth buffer (e.g. GL_DEPTH_COMPONENT32F , DXGI_FORMAT_D32_FLOAT_S8X24_UINT , MTLPixelFormat.depth32Float  etc.)

    • Finite :

      • Right Handed :

        @(require_results)
        camera_perspective_vulkan_reversed_z :: proc "contextless" (vertical_fov, aspect, near, far: f32) -> (m: Mat4) #no_bounds_check {
            /*
            Right-handed (Camera forward -Z).
            Clip Space: left-handed, y-down, with Z (depth) extending from 1.0 (close) to 0.0 (far).
            */
            /*
            This procedure assumes near < far.
            If near > far, the depth will be inverted, and far away geometry will be drawn first; which is incorrect.
            */
            return camera_perspective_vulkan_classical_z(vertical_fov, aspect, far, near)
        }
        
    • Infinite :

      • Right Handed :

        @(require_results)
        camera_perspective_vulkan_reversed_z_infinite :: proc "contextless" (vertical_fov, aspect, near: f32) -> (m: Mat4) #no_bounds_check {
            /*
            Right-handed (Camera forward -Z).
            Clip Space: left-handed, y-down, with Z (depth) extending from 1.0 (close) to 0.0 (far).
            */
            focal_length := 1.0 / math.tan(0.5 * vertical_fov)
            A :: 0
            B := near
            m[0, 0] = focal_length / aspect
            m[1, 1] = -focal_length
            m[2, 2] = A
            m[3, 2] = -1
            m[2, 3] = B
            return
        }
        
        • Vulkan Reversed-Z Infinite - Vincent Parizet .

          • I tested his version and it doesn't work for me.

          • If I transpose his version, it becomes exactly equal to the implementation above, and it works fine.

          • His matrices are probably row-major, not column-major.

  • Orthographic :

    • Right Handed :

      /// Clip-space: left-handed and y-down with Z (depth) clip extending from 0.0 (close) to 1.0 (far).
      #[inline]
      pub fn orthographic_vk(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) -> Mat4 {
          let rml = right - left;
          let rpl = right + left;
          let tmb = top - bottom;
          let tpb = top + bottom;
          let fmn = far - near;
          Mat4::new(
              Vec4::new(2.0 / rml, 0.0, 0.0, 0.0),
              Vec4::new(0.0, -2.0 / tmb, 0.0, 0.0),
              Vec4::new(0.0, 0.0, -1.0 / fmn, 0.0),
              Vec4::new(-(rpl / rml), -(tpb / tmb), -(near / fmn), 1.0),
          )
      }
      
DirectX / Metal
  • Uses 0 for the near plane and 1 for the far plane.

  • Classical-Z :

    • Finite :

      • Right Handed :

        // The resulting depth values mapped from 0 to 1.
        mat4 perspective_direct3d_rh(const float fovy, const float aspect, const float n, const float f) {
          const float e = 1.0f / std::tan(fovy * 0.5f);
          return {e / aspect, 0.0f,  0.0f,             0.0f,
                  0.0f,       e,     0.0f,             0.0f,
                  0.0f,       0.0f,  f / (n - f),      -1.0f,
                  0.0f,       0.0f, (f * n) / (n - f), 0.0f};
        }
        
        /// Meant to be used with WebGPU or DirectX.
        /// Clip-space: left-handed and y-up with Z (depth) clip extending from 0.0 (close) to 1.0 (far).
        #[inline]
        pub fn perspective_wgpu_dx(vertical_fov: f32, aspect_ratio: f32, z_near: f32, z_far: f32) -> Mat4 {
            let t = (vertical_fov / 2.0).tan();
            let sy = 1.0 / t;
            let sx = sy / aspect_ratio;
            let nmf = z_near - z_far;
        
            Mat4::new(
                Vec4::new(sx, 0.0, 0.0, 0.0),
                Vec4::new(0.0, sy, 0.0, 0.0),
                Vec4::new(0.0, 0.0, z_far / nmf, -1.0),
                Vec4::new(0.0, 0.0, z_near * z_far / nmf, 0.0),
            )
        }
        
      • Left Handed :

        // The resulting depth values mapped from 0 to 1.
        mat4 perspective_direct3d_lh(const float fovy, const float aspect, const float n, const float f) {
          const float e = 1.0f / std::tan(fovy * 0.5f);
          return {e / aspect, 0.0f,  0.0f,             0.0f,
                  0.0f,       e,     0.0f,             0.0f,
                  0.0f,       0.0f,  f / (f - n),      1.0f,
                  0.0f,       0.0f, (f * n) / (n - f), 0.0f};
        }
        
    • Infinite :

      • Right Handed :

        /// Meant to be used with WebGPU, or DirectX.
        /// Clip-space: left-handed and y-up with Z (depth) clip extending from 0.0 (close) to 1.0 (far).
        #[inline]
        pub fn perspective_infinite_z_wgpu_dx(vertical_fov: f32, aspect_ratio: f32, z_near: f32) -> Mat4 {
            let t = (vertical_fov / 2.0).tan();
            let sy = 1.0 / t;
            let sx = sy / aspect_ratio;
        
            Mat4::new(
                Vec4::new(sx, 0.0, 0.0, 0.0),
                Vec4::new(0.0, sy, 0.0, 0.0),
                Vec4::new(0.0, 0.0, -1.0, -1.0),
                Vec4::new(0.0, 0.0, -z_near, 0.0),
            )
        }
        
  • Reverse-Z :

    • Clear depth to 0 (not 1 as usual).

    • Set depth test to greater  (not less  as usual).

    • Ensure you’re using a floating point depth buffer (e.g. GL_DEPTH_COMPONENT32F , DXGI_FORMAT_D32_FLOAT_S8X24_UINT , MTLPixelFormat.depth32Float  etc.)

    • Finite :

      • Right Handed :

        /// Meant to be used with WebGPU, OpenGL, or DirectX.
        /// Clip-space: left-handed and y-up with Z (depth) clip extending from 0.0 (close) to 1.0 (far).
        /// Note: In order for this to work properly with OpenGL, you'll need to use the `gl_arb_clip_control` extension 
        /// and set the z clip from 0.0 to 1.0 rather than the default -1.0 to 1.0.
        #[inline]
        pub fn perspective_reversed_z_wgpu_dx_gl(
            vertical_fov: f32,
            aspect_ratio: f32,
            z_near: f32,
            z_far: f32,
        ) -> Mat4 {
            let t = (vertical_fov / 2.0).tan();
            let sy = 1.0 / t;
            let sx = sy / aspect_ratio;
            let nmf = z_near - z_far;
        
            Mat4::new(
                Vec4::new(sx, 0.0, 0.0, 0.0),
                Vec4::new(0.0, sy, 0.0, 0.0),
                Vec4::new(0.0, 0.0, -z_far / nmf - 1.0, -1.0),
                Vec4::new(0.0, 0.0, -z_near * z_far / nmf, 0.0),
            )
        }
        
    • Infinite :

      • Right Handed :

        /// Meant to be used with WebGPU, OpenGL, or DirectX.
        /// Clip-space: left-handed and y-up with Z (depth) clip extending from 0.0 (close) to 1.0 (far).
        /// Note: In order for this to work properly with OpenGL, you'll need to use the `gl_arb_clip_control` extension 
        /// and set the z clip from 0.0 to 1.0 rather than the default -1.0 to 1.0.
        #[inline]
        pub fn perspective_reversed_infinite_z_wgpu_dx_gl(
            vertical_fov: f32,
            aspect_ratio: f32,
            z_near: f32,
        ) -> Mat4 {
            let t = (vertical_fov / 2.0).tan();
            let sy = 1.0 / t;
            let sx = sy / aspect_ratio;
        
            Mat4::new(
                Vec4::new(sx, 0.0, 0.0, 0.0),
                Vec4::new(0.0, sy, 0.0, 0.0),
                Vec4::new(0.0, 0.0, 0.0, -1.0),
                Vec4::new(0.0, 0.0, z_near, 0.0),
            )
        }
        
  • Orthographic :

    • Right Handed :

      /// Clip-space: left-handed and y-up with Z (depth) clip extending from 0.0 (close) to 1.0 (far).
      #[inline]
      pub fn orthographic_wgpu_dx(
          left: f32,
          right: f32,
          bottom: f32,
          top: f32,
          near: f32,
          far: f32,
      ) -> Mat4 {
          let rml = right - left;
          let rpl = right + left;
          let tmb = top - bottom;
          let tpb = top + bottom;
          let fmn = far - near;
          Mat4::new(
              Vec4::new(2.0 / rml, 0.0, 0.0, 0.0),
              Vec4::new(0.0, 2.0 / tmb, 0.0, 0.0),
              Vec4::new(0.0, 0.0, -1.0 / fmn, 0.0),
              Vec4::new(-(rpl / rml), -(tpb / tmb), -(near / fmn), 1.0),
          )
      }
      
Legacy OpenGL
  • Uses −1 for the near and 1 for the far. This is the worst for floating point precision.

  • Classical-Z :

    • Finite :

      • Right-handed :

        // The resulting depth values mapped from -1 to +1.
        mat4 perspective_opengl_rh(const float fovy, const float aspect, const float n, const float f) {
          const float e = 1.0f / std::tan(fovy * 0.5f);
          return {e / aspect, 0.0f,  0.0f,                    0.0f,
                  0.0f,       e,     0.0f,                    0.0f,
                  0.0f,       0.0f, (f + n) / (n - f),       -1.0f,
                  0.0f,       0.0f, (2.0f * f * n) / (n - f), 0.0f};
        }
        
      • Left-handed :

        // The resulting depth values mapped from -1 to +1.
        mat4 perspective_opengl_lh(const float fovy, const float aspect, const float n, const float f) {
          const float e = 1.0f / std::tan(fovy * 0.5f);
          return {e / aspect, 0.0f,  0.0f,                    0.0f,
                  0.0f,       e,     0.0f,                    0.0f,
                  0.0f,       0.0f, (f + n) / (f - n),        1.0f,
                  0.0f,       0.0f, (2.0f * f * n) / (n - f), 0.0f};
        }
        
      • Normalizing Matrix :

        // map from -1 to 1 to 0 to 1
        mat4 normalize_unit_range(const mat4& perspective_projection)
        {
          constexpr mat4 normalize_range {1.0f, 0.0f, 0.0f, 0.0f,
                                          0.0f, 1.0f, 0.0f, 0.0f,
                                          0.0f, 0.0f, 0.5f, 0.0f,
                                          0.0f, 0.0f, 0.5f, 1.0f};
          return mat_mul(perspective_projection, normalize_range);
              // The correct multiplication is normalize_range * perspective_projection (column-vector math)
        }
        
    • Infinite :

      • Right-handed :

        /// Clip-space: left-handed and y-up with Z (depth) clip extending from -1.0 (close) to 1.0 (far).
        #[inline]
        pub fn perspective_infinite_z_gl(vertical_fov: f32, aspect_ratio: f32, z_near: f32) -> Mat4 {
            let t = (vertical_fov / 2.0).tan();
            let sy = 1.0 / t;
            let sx = sy / aspect_ratio;
        
            Mat4::new(
                Vec4::new(sx, 0.0, 0.0, 0.0),
                Vec4::new(0.0, sy, 0.0, 0.0),
                Vec4::new(0.0, 0.0, -1.0, -1.0),
                Vec4::new(0.0, 0.0, -2.0 * z_near, 0.0),
            )
        }
        
  • Reverse-Z :

    • Use the Normalizing Matrix above, and the Reverse-Z Matrix below, to get the final Proj Matrix.

    • We create a standard perspective matrix and update the depth mapping to be from -1 to 1 to 0 to 1 (all the matrix is doing is multiplying by 0.5 then adding 0.5 to achieve this).

    const mat4 perspective_projection = perspective_opengl_rh(radians(60.0f), float(width) / float(height), near, far);
    const mat4 reverse_z_perspective_projection = reverse_z(normalize_unit_range(perspective_projection));
    
    • Steps :

      • Clear depth to 0 (not 1 as usual).

      • Set depth test to greater  (not less  as usual).

      • Ensure you’re using a floating point depth buffer (e.g. GL_DEPTH_COMPONENT32F , DXGI_FORMAT_D32_FLOAT_S8X24_UINT , MTLPixelFormat.depth32Float  etc.)

      • (in OpenGL) Make sure glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE);  is set so OpenGL knows the depth range will be 0 to 1 and not -1 to 1.

      • Extra that I saw somewhere: you'll need to use the gl_arb_clip_control  extension and set the z clip from 0.0 to 1.0 rather than the default -1.0 to 1.0

  • Orthographic :

    • Right-handed :

      /// Clip-space: left-handed and y-up with Z (depth) clip extending from -1.0 (close) to 1.0 (far).
      #[inline]
      pub fn orthographic_gl(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) -> Mat4 {
          let rml = right - left;
          let rpl = right + left;
          let tmb = top - bottom;
          let tpb = top + bottom;
          let fmn = far - near;
          let fpn = far + near;
          Mat4::new(
              Vec4::new(2.0 / rml, 0.0, 0.0, 0.0),
              Vec4::new(0.0, 2.0 / tmb, 0.0, 0.0),
              Vec4::new(0.0, 0.0, -2.0 / fmn, 0.0),
              Vec4::new(-(rpl / rml), -(tpb / tmb), -(fpn / fmn), 1.0),
          )
      }
      
Reverse-Z
  • 1 for the near plane and 0 for the far plane.

  • .

  • .

  • .

  • Reverse Z Matrix :

    • We can just swap the near  and far  parameters when calling the function.

    • Or we can create a new matrix that will produce a reverse Z friendly matrix for us.

    • This matrix flips the z/depth value to go from 1 to 0 instead of 0 to 1.

    mat4 reverse_z(const mat4& perspective_projection)
    {
      constexpr mat4 reverse_z {1.0f, 0.0f,  0.0f, 0.0f,
                                0.0f, 1.0f,  0.0f, 0.0f,
                                0.0f, 0.0f, -1.0f, 0.0f,
                                0.0f, 0.0f,  1.0f, 1.0f};
      return mat_mul(perspective_projection, reverse_z);
          // The correct multiplication is reverse_z * perspective_projection (column-vector math)
    }
    
    • Note :

      • I tested doing this, but it simply didn't work.

  • Precision :

    • Results in a better distribution of the floating point values than using −1 and 1 or 0 and 1.

    • There is a truly insane  amount of precision near 0 with floating point numbers.

    • Out of the total range between 0.0 and 1.0, only approximately 0.79%  of all representable values are between 0.5 and 1.0, with a staggering 99.21%  between 0.0 and 0.5.

    • I always knew there was more precision near 0, but I don’t think I’d fully appreciated by quite how much.

    • Example :

      • Non-Reversed-Z:

        • If we choose a generous near clip plane of 1.0 and far clip plane of 100.0, and give an input value of say 5.0 to this equation we get an interesting result. The depth value is 0.808! That means even when we’re using regular z, and we have a value near the clip plane, we already have completely discarded an insane amount of precision because all those values near 0 already can’t be used. If we put in more representative numbers things get even worse. With a near clip plane of 0.01 and a far clip plane of 1000.0, using a view space depth value of 5.0 gives 0.998. That means we only have 33554 unique values left to represent normalized depth between 5.0 and 1000 which isn’t great.

      • Reversed-Z:

        • Now for the near clip plane at 1.0 and far clip plane at 100.0 case, with an input value of 5.0, we get approximately 0.192 for the depth value. If we set the near clip plane to 0.01 and the far clip plane to 1000, a view space depth value of 5.0 becomes approximately 0.00199. The amazing thing though is in this case we have 990014121 possible unique depth values, an improvement of 29500x over regular z.

        • Reversing z has the effect of smoothing the precision throughout the range (we leverage the fact so much precision sits between 0.0 and 0.5 by ensuring more values end up there). Without reverse z, the precision is front-loaded to the near clip plane where we don’t need as much precision.

        • The incredible thing about this is we gain all this extra precision and improved fidelity at no cost in performance or memory, which usually never happens in computer science or software engineering

  • Usage in Orthographic Projection :

    • The depth precision gain is negligible. Orthographic mapping is linear in z, so the standard forward mapping already distributes precision uniformly.

    • You should only do it if your renderer globally uses reverse-Z for consistency (e.g. shared depth buffer with perspective passes), otherwise, keep Classical-Z.

Infinite Perspective
  • Having to set scene-specific values for both the near and far plane can be a pain in the ass. If you want to display large open-world scenes, you will almost always use an absurdly high value for the far plane anyway.

  • With the increased precision of using a Reverse-Z, it is possible to set an infinitely distant far plane.

  • .

  • Usage in Orthographic Projection :

    • Orthographic projections define a finite slab of space.  Sending far → ∞  removes the far clipping plane, but all geometry at any z beyond near  will still map to the same NDC z value (0 or 1 depending on convention).

    • Depth test stops discriminating depth beyond the near plane, so almost everything “at infinity” z-fights.

    • It is mathematically valid but useless for depth testing. It can be used only if you never rely on depth ordering—e.g. for skybox or background plane rendering.

Nvidia Recommendations
  • Article .

  • The 1/z  (Reversed-Z) mapping and the choice of float versus integer depth buffer are a big part of the precision story, but not all of it. Even if you have enough depth precision to represent the scene you're trying to render, it's easy to end up with your precision controlled by error in the arithmetic of the vertex transformation process.

  • As mentioned earlier, Upchurch and Desbrun  studied this and came up with two main recommendations to minimize roundoff error:

    1. Use an infinite far plane.

    2. Keep the projection matrix separate from other matrices, and apply it in a separate operation in the vertex shader, rather than composing it into the view matrix.

      • In many cases, separating the view and projection matrices (following Upchurch and Desbrun’s recommendation) does make some improvement. While it doesn't lower the overall error rate, it does seem to turn swaps into indistinguishables, which is a step in the right direction.

  • Upchurch and Desbrun came up with these recommendations through an analytical technique, based on treating roundoff errors as small random perturbations introduced at each arithmetic operation, and keeping track of them to first order through the transformation process.

  • float32 or int24 :

    • There is no difference between float and integer depth buffers in most setups. The arithmetic error swamps the quantization error. In part this is because float32 and int24 have almost the same-sized ulp in [0.5, 1] (because float32 has a 23-bit mantissa), so there actually is almost no additional quantization error over the vast majority of the depth range.

  • Infinite Perspective :

    • An infinite far plane makes only a miniscule difference in error rates. Upchurch and Desbrun predicted a 25% reduction in absolute numerical  error, but it doesn't seem to translate into a reduced rate of comparison  errors.

  • Reverse-Z :

    • The reversed-Z mapping is basically magic.

    • Reversed-Z with a float depth buffer gives a zero error rate  in this test. Now, of course you can make it generate some errors if you keep tightening the spacing of the input depth values. Still, reversed-Z with float is ridiculously more accurate than any of the other options.

    • Reversed-Z with an integer depth buffer is as good as any of the other integer options.

    • Reversed-Z erases the distinctions between precomposed versus separate view/projection matrices, and finite versus infinite far planes. In other words, with reversed-Z you can compose your projection matrix with other matrices, and you can use whichever far plane you like, without affecting precision at all.

  • Conclusion :

    • In any perspective projection situation, just use a floating-point depth buffer with reversed-Z! And if you can't use a floating-point depth buffer, you should still use reversed-Z. It isn't a panacea for all precision woes, especially if you're building an open-world environment that contains extreme depth ranges. But it's a great start.

View Frustum

  • It's a region  in Camera / View / Eye Space ; it's not a matrix or a separated space.

  • A view frustum is the truncated-pyramid volume (for perspective cameras) or box (for orthographic cameras) that defines the region of space potentially visible to the camera.

  • Objects outside it are clipped or culled.

  • Perspective View Volume :

    • The 3D region produced by a perspective camera: geometrically a truncated pyramid (frustum).

    • Points in view (camera/eye) space that lie inside this frustum are potentially visible and then mapped by the perspective projection matrix into the canonical volume (NDC).

    • This term is essentially the same concept as View Frustum  for perspective cameras.

    • Parameters :

      • Field of view (or focal length), aspect ratio, near plane, far plane.

  • Orthographic View Volume :

    • A rectangular box (prism) defining the visible region for an orthographic (parallel) projection.

    • Unlike the perspective frustum, there is no perspective foreshortening; parallel lines remain parallel.

    • The orthographic view volume is mapped to the canonical view volume by the orthographic projection matrix.

    • Parameters :

      • Left, right, top, bottom, near, far.

  • Plane representation :

    • The frustum can be represented by six plane equations. Those are convenient for fast culling and intersection tests.

  • Frustum culling :

    • Is a performance test done usually on the CPU/GPU before or during rendering.

    • It tests bounding volumes (AABBs/spheres) against the six frustum planes (left/right/top/bottom/near/far) to skip drawing objects outside the frustum.

    • Those plane tests can be done in Camera / View / Eye Space  (transform object bounds by the view matrix) or in World Space  (transform the frustum planes into world space by the inverse view).

Orthographic Projection Matrix

  • A 4×4 matrix that implements an orthographic projection.

  • It uses linear (non-perspective) mapping in z and x/y and does not produce perspective foreshortening.

Perspective Projection Matrix

  • A 4×4 matrix that implements a perspective projection.

  • .

  • .

MVP Matrix / Model-View-Projection Matrix

  • The combined matrix (Projection × View × Model).

  • Multiply the model-space vertex by the MVP to get Clip Space  homogeneous coordinates; it's not in NDC Space .

  • glm::mat4 MVPmatrix = projection * view * model; // Remember: inverted!

  • Using a single combined matrix is common for efficiency.

Clip Space

  • 4D Homogeneous space.

  • Clip Space Position :

    • It's the gl_Position  in GLSL.

  • Projected Frustum / Canonical Clip Volume :

    • Exists in Clip Space  after applying the Projection Matrix  to the View Frustum .

    • The View Frustum  is remapped so that:

      • Left, right, top, bottom planes align with x=±w , y=±w .

      • Near, far planes align with z=±w  (OpenGL) or z=[0,w]  (D3D/Vulkan).

    • It's the region  the GPU will Clip  against.

  • Clipping :

    • Is performed in clip space (or against clip planes).

    • Removes geometry outside the canonical clip volume (the projected Frustum).

    • This avoids producing fragments that should not be visible and prevents division-by-zero problems.

    • When to clip :

      • Reason it happens in clip space: the clip planes are linear in homogeneous coords; doing clipping after perspective divide would be non-linear and problematic.

      • Clipping in homogeneous clip space uses linear plane equations (easy and reliable).

      • If you divided first, you would get nonlinear boundaries and more complicated intersection math; also you could divide by 0 for points on the camera plane.

Perspective Divide

  • It's an operation to convert homogeneous clip coordinates to 3D (from Clip Space  to Canonical View Volume / NDC / Normalized Device Coordinates ).

  • It's just an operation.

  • Divide x , y , z  by w  from the Clip Space  to get the Canonical View Volume / NDC / Normalized Device Coordinates .

  • Once you perform the Perspective Divide , that Projected Frustum  becomes the NDC Cube / Canonical View Volume .

NDC Space / Normalized Device Coordinates Space

  • 3D non-homogeneous space.

  • Is the result derived from Clip Space  by the Perspective Divide .

  • Geometry that lies inside the NDC Space  will map into the Viewport .

  • NDC Cube / Canonical View Volume :

    • Obtained after performing the Perspective Divide  in the Projected Frustum .

    • Is convenient because the next mapping to pixels is a simple affine transform.

  • Coordinates :

    • After a vertex is multiplied with this matrix X and Y, the resulting coordinates are position on the screen (between [-1, 1] ).

    • The Z is used for the depth-buffer and identifies how far the vertex (or fragment) is from your camera's near plane.

  • Typical ranges :

    • OpenGL convention:

      • x ∈ [-1,1] , y ∈ [-1,1] , z ∈ [-1,1] .

    • Direct3D / Vulkan convention:

      • x ∈ [-1,1] , y ∈ [-1,1] , z ∈ [0,1]  (Z-range differs).

Viewport Transform Matrix

  • Transforms NDC Space / Normalized Device Coordinates Space / Canonical View Volume  to Window Coordinates / Screen Coordinates .

  • This transform is linear in X and Y but affine in Z, mapping NDC z-range to depth buffer range ( [0,1]  or [-1,1] ).

  • This is the final transform before rasterization writes to the framebuffer.

  • The projection stage produces normalized coordinates that are independent of the actual render target size or position.

  • This transform gives those normalized coordinates a real position and scale on a particular render target (pixel grid + depth range).

  • Without viewport mapping, you would only have coordinates in [-1,1]  ( NDC Space ); the rasterizer needs actual pixel locations (and a depth value in the depth-buffer range) to generate fragments and write pixels.

  • The GPU/driver usually applies this mapping automatically (parameters set by API calls such as glViewport  / vkCmdSetViewport ), but conceptually it is an affine transform applied after the Perspective Divide .

Screen Space / Screen Coordinates / Window Coordinates

  • 2D coordinates in pixels (and a depth value).

  • It's the final stage before rasterization.

What Vulkan does implicitly

  • These are built-in, automatic steps the GPU will perform after  your Vertex Shader  runs, as part of the fixed-function pipeline:

  • Clipping :

    • Vulkan clips  primitives against the Projected Frustum / Canonical Clip Volume  automatically, based on the convention:

      • x,y ∈ [−w,w] , z ∈ [0,w]  in Clip Space .

    • This happens whether you use Perspective Projection  or Orthographic Projection .

  • Perspective Divide :

    • Vulkan automatically divides x , y , z  by w  from the Clip Space  to get the NDC Space .

  • Viewport Transform Matrix :

    • Vulkan uses the VkViewport  parameters you provide to map NDC Space  to Screen Space / Screen Coordinates / Window Coordinates .

    • It also maps NDC Cube  Z from [0, 1]  to your depth-buffer range (possibly reversed if you configure it that way).

Common Techniques

Interpolation, Blending

Interpolation
Blend Modes
Pre-multiplied Alpha
  • Normally, a color with alpha is stored as:

RGBA = (R, G, B, A)
  • In premultiplied alpha, the RGB channels are already multiplied by alpha:

RGBA_premultiplied = (R*A, G*A, B*A, A)
  • This changes how blending calculations behave because the transparency is “baked into” the color channels.

  • Pre-multiplying the alpha in a frag shader :

    vec4 color = texture(sampler, uv);
    color.rgb *= color.a;
    
  • In Vulkan :

    • Vulkan does not  automatically premultiply alpha for you.

    • Vulkan itself only handles blending according to the factors you specify in the pipeline, after  the fragment shader.

    • Vulkan blending formula:

      finalColor = srcColor * srcFactor + dstColor * dstFactor
      
    • For premultiplied alpha, typical blending factors are:

      srcColor = ONE
      dstColor = ONE_MINUS_SRC_ALPHA
      
    • Without premultiplied alpha (standard alpha):

      srcColor = SRC_ALPHA
      dstColor = ONE_MINUS_SRC_ALPHA
      

Precision

  • A float has 24 bits of mantissa (≈7 decimal digits). Precision matters because large coordinate values consume more of the float’s exponent, leaving fewer bits for fractional accuracy. Floats have finer resolution near zero and coarser resolution far away. One analysis shows that if 1 unit = 1 mm, at 1 km (1,000,000 mm) float precision is ±0.125 mm, and at 64 km it’s ±4 mm. Thus large world coordinates effectively “quantize” small movements.

  • Because float positions quantize as you move objects far from the origin, subtle interpolation or coverage tests can be off by a pixel. In tile or 2D sprite rendering, this shows up as white lines or flicker between sprites. In 3D this is known as “z-fighting”; in 2D it’s the same principle in XY.

Options
  1. Recenter your origin: Keep the camera near (0,0) and subtract the camera position from object positions before sending to the GPU. In essence, you recenter your origin around the player or view. As GClements advises, always store or compute positions relative  to a nearby origin so you’re not adding enormous offsets and losing precision.

  2. Tweak UVs: For texture atlas or sprite sheets, slightly inset the UVs (e.g. 0.01 px) to avoid bleeding from neighboring texels as a workaround.

  3. Share geometry edges: When building tile meshes, reuse the same vertex for adjacent tiles so FP rounding can’t create tiny gaps

  4. Chunking: If your dataset spans a huge area, the solution is to split it into chunks, with the data in each chunk referenced relative to the origin of the chunk. Any rendering will either only use a small number of chunks surrounding the point of interest, or (for a large-scale view) won’t require millimeter precision (your monitor simply doesn’t have enough pixels for that).

  5. If you can, try using a 0.0-0.5 range.

  6. Double precision on CPU: Keep objects’ true positions in double precision on the CPU and feed offset-from-camera positions as floats to the GPU

Single-precision floats vs Double-precision floats
  • Godot - Table of Single-precision vs Double-precision .

  • In modern graphics shaders (GLSL/Vulkan) float  is 32-bit by default. Double-precision ( double , dvec ) exists on desktop GL/GLSL (with GLARBgpushaderfp64 ) but is almost never available or practical on mobile. In fact, mobile GPUs generally do not support 64-bit floats in shaders. The Android Vulkan guide explicitly states that 64-bit floats are “not commonly supported” on Android devices and not recommended

  • A survey of Vulkan device capabilities shows none of the major mobile vendors (Arm Mali, Qualcomm Adreno, PowerVR, Apple) support shaderFloat64, only desktop GPUs do.

  • Thus you must use single-precision on mobile Vulkan; double math is only possible on the CPU side or via emulation (which is slow).

  • Another mobile-specific concern is GLSL precision qualifiers (in OpenGL ES). On ES2.0 hardware, lowp  floats may only guarantee the range [-2,2], and even mediump  is limited (≈±2^14). In practice, any ES2.0-capable GPU supports mediump  in all shaders, so world coordinates should use highp  (32-bit) or mediump  where highp  is unavailable. In Vulkan GLSL, though, floats are full 32-bit by default (there is no mediump  reduction unless using specific extensions). The key is: use full 32-bit precision for positions.

Noise

Interleaved Gradient Noise (IGN)
  • .

  • "IGN is God sent".

  • Also works better than the alternatives with blur effects.

  • .

Perlin Noise
Seamless Perlin Noise
Fractal Brownian Motion (fBM)
Random Value
Voronoi and Worley (cellular) Noise

Gaussian Splatting

  • Point clouds.

  • Exported as a .ply  file.

  • Dynamic 3D Gaussians .

  • Gaussian Splatting .

    • A demonstration of how to do it is shown.

    • The software used is called PostShot.

    • His kitchen scene ended up being 122mb in size.

    • It shows how to export to Blender, etc.

Shaders

Snow
Ice
Water
Rain
Sun Beams
  • These are accomplished with a radial blur centered on the sun, during postprocessing. The nice thing about this effect is how dynamic it is. As you walk beneath the trees, the shape and intensity of the beams constantly change.

Fog
// Linearize the depth factor
depth = (1 - far/near) * depth + (far/near);
depth = 1.0 / depth;

// Multiply by the far clipping plane distance.
depth *= far;
// This results in the `view_distance`.
view_distance = depth;
  • 3 possible formulas:

    • .

    • Linear.

    • Exponential.

    • Exponential Squared.

      • This one is recommended because it has a nice effect.

Spatial
Grass
  • Instantiate grass and make it billboarded .

    • He does not show any code, but says he uses the following things:

      1. Instantiates objects evenly on the terrain, 'somehow'.

        • I believe the same method he used to 'instance foliage on a tree' is used.

      2. Makes the objects have size variation and so on, to give variety and the impression of more organic foliage.

      3. Accesses the position of each blade and makes it have Y-Billboarding, 'somehow'.

      4. The color of each blade is based on the color of the terrain below where it was instanced.

        • This was done by placing a camera above the terrain, so that what it sees is used as a sample.

    • No grass interacts with physics.

  • Grass movement, considering the presence of the player .

Foliage
See-through
Cut-off
Screen Filters
2D Dissolve
2D Water
2D Wind
2D Physics Rendering
2D Etc
Particles
Scene Transitions
Techniques

PS1

  • PS1 Graphics {10:56} - Acerola .

    • The start of the video gives an overview of graphics pipelines and transformation matrices.

      • The explanation is ultra rushed and makes it seem much more complicated than it is.

    • PS1 used a 2D renderer and had some limitations:

      • Affine Texture Mapping

        • Modern GPUs do not have this issue as they can properly interpolate perspective-correct values, since they are meant to render 3D scenes.

        • To simulate this we multiply the UV coordinates by w in the vertex shader and in the fragment shader we divide by w to reverse the perspective correction applied during interpolation in rasterization.

          • The distortion effect can be really severe, but using meshes with higher polygon count reduces the effect.

          • A tessellation shader could correct this distortion almost entirely, but since we want the effect, we'll keep it as is.

      • Integer coordinate restriction.

        • To simulate this, a vertex shader is used to snap to the nearest value based on the screen resolution.

    • For lighting, we'll use vertex lighting.

      • This means we can only have a single light source.

      • We'll use this for ambient occlusion.

    • Fog is made...

    • For color, we convert the default 16-bit color to 5-bit color:

      • .

      • From [0,255]  to [0,31] .

    • Finally, a dither is applied to reduce the color banding caused by 5-bit colors.

  • PS1 Graphics - Implementation by Acerola in Godot .

    • Cool. Can be used as a guide. It is less formal and confusing than Acerola.

  • PS1 Texturing Tip .

    • The only relevant part of the video is the technique to make the truck's texture, and nothing else.

Pixel Art

Sources
  • t3ssel8r .

    • Often attributes much more mathematics than necessary to certain topics, with little focus on design dilemmas, and a lot of focus on mathematical over-complication.

    • Clearly a mathematician inspired by 3Blue1Brown.

    • Often flexes about the time spent on certain tasks; there is a big ego.

  • Voyage .

  • aarthificial .

Fixing the "shimmering artifact"
Camera Smoothness
Etc